"""
@breif 提供一组便于在运行时改变程序参数的通用工具
@description
    对于具体的任务而言，识别算法很重要，算法的参数同样很重要。为了便于在树莓派上快速调试这些参数，
    通过网页而不是远程桌面来修改参数，因此就要求程序有动态改变运行中变量的接口。修改之后，将参数
    保存在文件中，下次自动加载也将大大方便程序的使用。为此将相关功能编写成本模块，提供一些通用的
    功能，以提高代码的复用性。
@author Stone at BlueNet, GDOU
@date Nov. 2020
"""
from copy import deepcopy

import logging
import json
import os


JSON_INDENT = 4  # json文件中, 缩进的空格数


logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)


class ParameterError(Exception):
    pass


class BaseParameters(object):
    def __init__(self, params):
        self._params = deepcopy(params)
        
    def __getattr__(self, name):
        # 为避免与self._params中的键名冲突, 本类及其所有子类的方法都要加下划线.
        if name in self._params:
            return self._params[name]
        
        raise KeyError("key '%s' no found" % name)
    

class DefaultParameters(BaseParameters):
    """为了明确哪些参数是通过本模块管理的，以及提供这些参数的默认值，要求应用程序首先通过本类定
    义默认参数。参数的实际值取决于参数文件（如果存在的话），而管理哪些参数，则以应用程序中，通过
    本类定义的实例为准。
    
    设计的使用方法是这样的:
    vision_params = DefaultParamters(
        redLower = [100, 98, 0],
        redUpper = [200, 200, 255]
        ...
    )
    
    之所以定义一个类,而不是直接用字典, 是因为出于以下两点考虑:
    1. 通过类的名称, 可以更清晰地表达该变量的意图;
    2. 通过关键字参数定义键值对, 而不是通过字符串定义键值对, 感觉会舒服些(至少可以少写一对引号)
    """
    def __init__(self, **default_params):
        super().__init__(default_params)


class Parameters(BaseParameters):
    def __init__(self, default_params:DefaultParameters, filename=None, auto_save=True):
        """
        如果``filename``指定的文件存在,则从该文件读取参数值,若不存在,则参数的取值为``default_params``.
        读取参数值后,会与``defaul_params``比较, 如果从文件中读取的参数的键与``default_params``不一致,
        则会报错. 如果后来修改了default_params, 则应将文件删除, 由程序重新生成(记得提前备份参数的值).
        
        :param filename: 参数文件的文件名，若不存在则自动创建
        :param default_params: 默认参数。
        :param auto_save: 是否在每次更新参数值之后, 自动保存到文件
        """
        super().__init__(dict())
        self._filename = filename
        self._auto_save = auto_save
        
        # 不需要保存文件的情况
        if self._filename is None:
            self._load = lambda *args, **kwargs: None
            self._save = lambda *args, **kwargs: None
            
            self._params = deepcopy(default_params._params)
        
        self._load(default_params)
    
    def __repr__(self):
        # very slow string operation
        ret = '%s\r\n' % self.__class__
        ret += '{\r\n'
        
        for k in self._params:
            ret += '    {}:{}\r\n'.format(k, self._params[k])
        
        ret += '}'
        return ret
    
    def _load(self, default_params):
        """若文件存在, 则读取参数, 若文件不存在, 则使用默认参数, 并将其写入文件"""
        if os.path.isfile(self._filename):
            logger.debug("load param")
            with open(self._filename) as f:
                self._params = json.load(f)
                
                f.close()
            
            # 检查一致性
            if self._params.keys() != default_params._params.keys():
                raise ParameterError("从参数文件导入的键与代码中定义的默认参数的键不一致,请删除掉参数文件, 由程序重新生成(不要忘记备份参数的值哟).")
        
        else:
            logger.debug("create param")
            self._params = deepcopy(default_params._params)
            self._save()
        
    def _save(self):
        with open(self._filename, 'w') as f:
            json.dump(self._params, f, indent=JSON_INDENT)
            f.close()
    
    def _update(self, params_dict:dict):
        """从字典更新参数的值"""
        self._params.update(params_dict)
        
        if self._auto_save:
            self._save()


if __name__ == '__main__':
    test_param = {
        'p1': 0,
        'p2': 0
    }
    
    default = DefaultParameters(**test_param, p3=3)
    
    print("test:\n", test_param)
    print()
    print("default:\n", default._params)
    
    param = Parameters(default, 'a.json')
    print(param)
    # param._load(default)
    
    
    # test if filename is None
    param = Parameters(default, None)
    param._load(default)
    param._save()



